// This Pine Script™ code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © fluxchart

//@version=5
const bool DEBUG = false
const int maxDistanceToLastBar = 5000
const int labelCooldown = 8
const int KDELimit = 300

indicator("RSI (Kernel Optimized) | Flux Charts", overlay = true, max_labels_count = 500)

rsiLengthInput = input.int(14, minval = 1, title="RSI Length", group="RSI Settings")
rsiSourceInput = input.source(close, "Source", group="RSI Settings")

highPivotLen = input.int(21,  "High Pivot Length", minval = 1, group = "Pivots", display = display.none)
lowPivotLen = input.int(21, "Low Pivot Length", minval = 1, group = "Pivots", display = display.none)
realPivotLabels = DEBUG ? input.bool(false, "[DBG] Real Pivot Labels", group = "Pivots") : false
kdePivotLabels = DEBUG ? input.bool(false, "[DBG] KDE Pivot Labels", group = "Pivots") : false

activationThresholdStr = input.string("Medium", "Activation Threshold", options = ["High", "Medium", "Low"], group = "KDE", tooltip = "Determines the amount of arrows shown. Higher options will result in more arrows being rendered.")
string KDEKernel = input.string("Gaussian", "Kernel", options=['Uniform', 'Gaussian', 'Sigmoid'], group = "KDE", tooltip = "The kernel function for KDE calculation. Gaussian is a commonly used kernel and is based on normal distribution.")
float KDEBandwidth = input.float(2.71828, "Bandwidth", group = "KDE", tooltip = "This setting sets the smoothness of the KDE function output.")
int KDEStep = input.int(100, "Nº Bins", minval = 1, group = "KDE", tooltip = "The number of elements the KDE Probability array will have. Higher settings will result in greater precision.")
activationThreshold = DEBUG ? input.float(0.25, "[DBG] Activation Threshold", group = "KDE") : 0.25
if not DEBUG
    activationThreshold := (activationThresholdStr == "High" ? 0.4 : activationThresholdStr == "Medium" ? 0.25 : 0.15)
probMode = DEBUG ? input.string("Sum", '[DBG] Probability Mode', options = ["Sum", "Nearest"], group = "KDE") : "Sum"
minPadding = DEBUG ? input.bool(false, '[DBG] KDE Min Padding', group = "KDE") : false

tableEnabled = input.bool(true, "Dashboard", group = "Dashboard", display = display.none)
tableLocation = input.string("Top Right", "Position", options = ["Top Right", "Top Center", "Right Center", "Middle Center", "Left Center", "Bottom Center"], group = "Dashboard", display = display.none)
screenerColor = input.color(#1B1F2B, 'Background',  group = 'Dashboard', display = display.none)
frameColor = input.color(color.rgb(255, 255, 255), 'Frame', group = 'Dashboard', display = display.none)
borderColor = input.color(color.rgb(255, 255, 255), 'Border', group = 'Dashboard', display = display.none)
textColor = input.color(color.white, 'Text', group = 'Dashboard', display = display.none)
fillBackgrounds = input.bool(true, "Fill Backgrounds", group = "Dashboard", display = display.none)

bearishColor = input.color(#f23646, "High Pivots", group = "Style", inline = "col", display = display.none)
neutralColor = input.color(color.gray, "Neutral", group = "Style", inline = "col", display = display.none)
bullishColor = input.color(#089981, "Low Pivots", group = "Style", inline = "col", display = display.none)
RSILabelsEnabled = input.bool(true, "RSI Labels", group = "Style")
KDELabelsEnabled = input.bool(true, "KDE Labels", group = "Style")

rsi = ta.rsi(rsiSourceInput, rsiLengthInput)

getPosition (positionText) =>
    if positionText == "Top Right"
        position.top_right
    else if positionText == "Top Center"
        position.top_center
    else if positionText == "Right Center"
        position.middle_right
    else if positionText == "Left Center"
        position.middle_left
    else if positionText == "Bottom Center"
        position.bottom_center
    else if positionText == "Middle Center"
        position.middle_center

//#region KDE
gaussian (float distance, float bandwidth = 1.0) => 1.0 / math.sqrt(2.0 * math.pi) * math.pow(math.e, -0.5 * math.pow(distance / bandwidth, 2.0))
uniform (float distance, float bandwidth = 1.0) => (math.abs(distance) > bandwidth) ? 0.0 : 0.5
sigmoid (float distance, float bandwidth = 1.0) => 2.0 / math.pi * (1.0 / (math.pow(math.e, (distance / bandwidth)) + math.pow(math.e, -(distance / bandwidth))))

kde (array<float> arr, string kernel, float bandwidth, int steps) =>
    arrSize = arr.size()
    arrRange = arr.range()
    arrMin = arr.min() - (minPadding ? (arrRange / 2.0) : 0)
    stepCount = arrRange / steps
    
    densityRange = array.new<float>(steps * 2)
    for i = 0 to (steps * 2) - 1
        densityRange.set(i, arrMin + i * stepCount)
    
    xArr = array.new<float>()
    yArr = array.new<float>()
    for i = 0 to densityRange.size() - 1
        float temp = 0
        for j = 0 to arr.size() - 1
            switch KDEKernel
                "Gaussian" => temp += gaussian(densityRange.get(i) - arr.get(j), 1.0 / bandwidth)
                "Uniform" => temp += uniform(densityRange.get(i) - arr.get(j), 1.0 / bandwidth)
                "Sigmoid" => temp += sigmoid(densityRange.get(i) - arr.get(j), 1.0 / bandwidth)
        
        xArr.push(densityRange.get(i))
        yArr.push(1.0 / arrSize * temp)
    [xArr, yArr]
//#endregion

//#region Pivots

prefixSum (array<float> arr, int l, int r) =>
    arr.get(r) - (l == 0 ? 0 : arr.get(l - 1))

float MidKDEHigh = na
float MidKDELow = na

var array<float> KDEHighX = na
var array<float> KDEHighY = na
var array<float> KDEHighYSum = array.new<float>()

var array<float> KDELowX = na
var array<float> KDELowY = na
var array<float> KDELowYSum = array.new<float>()

highPivot = ta.pivothigh(highPivotLen, highPivotLen)
lowPivot = ta.pivotlow(lowPivotLen, lowPivotLen)

var highPivotRSIs = array.new<float>()
var lowPivotRSIs = array.new<float>()

if not na(highPivot) 
    if highPivotRSIs.size() > KDELimit
        highPivotRSIs.remove(0)
    highPivotRSIs.push(rsi[highPivotLen])

    [KDEHighX1, KDEHighY1] = kde(highPivotRSIs, KDEKernel, KDEBandwidth, KDEStep)
    KDEHighX := KDEHighX1
    KDEHighY := KDEHighY1
    
    KDEHighYSum.clear()
    temp = 0.0
    for i = 0 to KDEHighY.size() - 1
        temp += KDEHighY.get(i)
        KDEHighYSum.push(temp)

    MidKDEHigh := array.get(KDEHighX, array.indexof(KDEHighY, array.max(KDEHighY)))

if not na(lowPivot)
    if lowPivotRSIs.size() > KDELimit
        lowPivotRSIs.remove(0)
    lowPivotRSIs.push(rsi[lowPivotLen])

    [KDELowX1, KDELowY1] = kde(lowPivotRSIs, KDEKernel, KDEBandwidth, KDEStep)
    KDELowX := KDELowX1
    KDELowY := KDELowY1

    KDELowYSum.clear()
    temp = 0.0
    for i = 0 to KDELowY.size() - 1
        temp += KDELowY.get(i)
        KDELowYSum.push(temp)

    MidKDELow := array.get(KDELowX, array.indexof(KDELowY, array.max(KDELowY)))
//#endregion

//#region KDE Optimization
f_lin_interpolate(float x0, float x1, float y0, float y1, float x) =>
    y0 + (x - x0) * (y1 - y0) / (x1 - x0)

float lowProb = na
float maxLowProb = na
float highProb = na
float maxHighProb = na

if last_bar_index - maxDistanceToLastBar < bar_index
    if highPivotRSIs.size() > 0
        highXIndexL = array.binary_search_leftmost(KDEHighX, rsi)
        highXIndexR = math.min(array.binary_search_rightmost(KDEHighX, rsi), KDEHighX.size() - 1)
        nearestIndex = (math.abs(rsi - KDEHighX.get(highXIndexL)) < math.abs(rsi - KDEHighX.get(highXIndexR))) ? highXIndexL : highXIndexR
        if probMode == "Nearest"
            highProb := KDEHighY.get(nearestIndex)
            maxHighProb := array.max(KDEHighY)
        else if probMode == "Sum"
            highProb := prefixSum(KDEHighYSum, 0, nearestIndex)
    
    if lowPivotRSIs.size() > 0
        lowXIndexL = array.binary_search_leftmost(KDELowX, rsi)
        lowXIndexR = math.min(array.binary_search_rightmost(KDELowX, rsi), KDELowX.size() - 1)
        nearestIndex = (math.abs(rsi - KDELowX.get(lowXIndexL)) < math.abs(rsi - KDELowX.get(lowXIndexR))) ? lowXIndexL : lowXIndexR
        if probMode == "Nearest"
            lowProb := KDELowY.get(nearestIndex)
            maxLowProb := array.max(KDELowY)
        else if probMode == "Sum"
            lowProb := prefixSum(KDELowYSum, nearestIndex, KDELowYSum.size() - 1)

        if DEBUG and barstate.islastconfirmedhistory
            for i = 0 to KDELowX.size() - 1
                curX = KDELowX.get(i)
                curY = KDELowY.get(i)
                log.info(str.tostring(curX) + " = " + str.tostring(curY))
            log.info("High Y Sum " + str.tostring(KDEHighY.sum()))

diffToHighKDE = math.abs(rsi - MidKDEHigh)
diffToLowKDE = math.abs(rsi - MidKDELow)
//#endregion

//#region Draw Pivots
color curColor = na
if (not na(KDELowY)) and (not na(KDEHighY))
    if probMode == "Nearest"
        if math.abs(lowProb - maxLowProb) < activationThreshold / 50.0
            curColor := bullishColor
        if math.abs(highProb - maxHighProb) < activationThreshold / 50.0
            curColor := bearishColor
    else if probMode == "Sum"
        if lowProb > KDELowY.sum() * (1.0 - activationThreshold)
            curColor := bullishColor
        else if highProb > KDEHighY.sum() * (1.0 - activationThreshold)
            curColor := bearishColor
//barcolor(curColor)

atr = ta.atr(50)

plotarrow(curColor == bullishColor and barstate.isconfirmed ? 1 : na, "Bullish Arrows", color.new(bullishColor, 70), color.new(bullishColor, 70), minheight = 20, maxheight = 20)
plotarrow(curColor == bearishColor and barstate.isconfirmed ? -1 : na, "Bearish Arrows", color.new(bearishColor, 70), color.new(bearishColor, 70), minheight = 20, maxheight = 20)

plotarrow((na(curColor) and curColor[1] == bullishColor and barstate.isconfirmed) ? 1 : na, "Possible Bullish Pivot", bullishColor, bullishColor, minheight = 20, maxheight = 20)
plotarrow((na(curColor) and curColor[1] == bearishColor and barstate.isconfirmed) ? -1 : na, "Possible Bearish Pivot", bearishColor, bearishColor, minheight = 20, maxheight = 20)

alertcondition(na(curColor) and curColor[1] == bullishColor and barstate.isconfirmed, "Possible Bullish Pivot")
alertcondition(na(curColor) and curColor[1] == bearishColor and barstate.isconfirmed, "Possible Bearish Pivot")

if KDELabelsEnabled or RSILabelsEnabled
    var lastBullishLabel = 0
    if (na(curColor) and curColor[1] == bullishColor and barstate.isconfirmed) and (bar_index - lastBullishLabel) > labelCooldown
        lastBullishLabel := bar_index
        txt = ""
        if RSILabelsEnabled and KDELabelsEnabled
            txt := "RSI | " + str.tostring(rsi, "#") + " | " + str.tostring(lowProb * 100, "#.##") + "%"
        else if RSILabelsEnabled
            txt := "RSI | " + str.tostring(rsi, "#")
        else
            txt := str.tostring(rsi, "#") + "%"
        label.new(bar_index, low, txt, yloc = yloc.belowbar, color = na, style = label.style_label_up, textcolor = color.white, force_overlay = true)
        
    var lastBearishLabel = 0
    if (na(curColor) and curColor[1] == bearishColor and barstate.isconfirmed) and (bar_index - lastBearishLabel) > labelCooldown
        lastBearishLabel := bar_index
        txt = ""
        if RSILabelsEnabled and KDELabelsEnabled
            txt := "RSI | " + str.tostring(rsi, "#") + " | " + str.tostring(highProb * 100, "#.##") + "%"
        else if RSILabelsEnabled
            txt := "RSI | " + str.tostring(rsi, "#")
        else
            txt := str.tostring(rsi, "#") + "%"
        label.new(bar_index, low, txt, yloc = yloc.abovebar, color = na, style = label.style_label_down, textcolor = color.white, force_overlay = true)

if kdePivotLabels
    txt = str.tostring(rsi, "#.##") + "\nHP -> " + str.tostring(highProb, "#.##") + "\nLP -> " + str.tostring(lowProb, "#.##") + "\n\nMHP -> " + str.tostring(maxHighProb, "#.##") + "\nMLP -> " + str.tostring(maxLowProb, "#.##")
    if math.abs(lowProb - maxLowProb) < activationThreshold
        label.new(bar_index, high, txt, yloc = yloc.belowbar, color = color.white, style = label.style_label_up, textcolor = color.black, force_overlay = true)
    if math.abs(highProb - maxHighProb) < activationThreshold
        label.new(bar_index, high, txt, yloc = yloc.abovebar, color = color.white, style = label.style_label_down, textcolor = color.black, force_overlay = true)

if realPivotLabels
    if not na(highPivot)
        txt = str.tostring(rsi[highPivotLen], "#.##") + "\nHP -> " + str.tostring(highProb[highPivotLen], "#.##") + "\nLP -> " + str.tostring(lowProb[highPivotLen], "#.##") + "\n\nMHP -> " + str.tostring(maxHighProb[highPivotLen], "#.##") + "\nMLP -> " + str.tostring(maxLowProb[highPivotLen], "#.##")
        label.new(bar_index - highPivotLen, high, txt, yloc = yloc.abovebar, color = color.white, style = label.style_label_down, textcolor = color.black, force_overlay = true)
    if not na(lowPivot)
        txt = str.tostring(rsi[lowPivotLen], "#.##") + "\nHP -> " + str.tostring(highProb[lowPivotLen], "#.##") + "\nLP -> " + str.tostring(lowProb[lowPivotLen], "#.##") + "\n\nMHP -> " + str.tostring(maxHighProb[lowPivotLen], "#.##") + "\nMLP -> " + str.tostring(maxLowProb[lowPivotLen], "#.##")
        label.new(bar_index - lowPivotLen, high, txt, yloc = yloc.belowbar, color = color.white, style = label.style_label_up, textcolor = color.black, force_overlay = true)
//#endregion

if tableEnabled
    var table realtimeTable = table.new(getPosition(tableLocation), 2, 10, bgcolor = screenerColor, frame_width = 2, frame_color = frameColor, border_width = 1, border_color = borderColor)
    // Header
    table.merge_cells(realtimeTable, 0, 0, 1, 0)
    table.cell(realtimeTable, 0, 0, "KDE Optimized RSI", text_color = color.white, bgcolor = screenerColor)

    // RSI
    table.cell(realtimeTable, 0, 1, "RSI", text_color = color.white, bgcolor = screenerColor)
    table.cell(realtimeTable, 1, 1, str.tostring(rsi, "#"), text_color = color.white, bgcolor = screenerColor)

    // KDE
    table.cell(realtimeTable, 0, 2, (lowProb > highProb) ?  "Bullish KDE" : "Bearish KDE", text_color = (lowProb > highProb) ?  bullishColor : bearishColor, bgcolor = screenerColor)
    table.cell(realtimeTable, 1, 2, str.tostring(nz(math.max(highProb, lowProb), 0) * 100, "#.##") + "%", text_color = color.white, bgcolor = screenerColor)